package org.sigmah.client.ui.widget;
/*
* #%L
* Sigmah
* %%
* Copyright (C) 2010 - 2016 URD
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program. If not, see
* <http://www.gnu.org/licenses/gpl-3.0.html>.
* #L%
*/
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import org.sigmah.client.i18n.I18N;
import org.sigmah.client.ui.widget.panel.ClickableFlowPanel;
import org.sigmah.shared.command.result.Authentication;
import org.sigmah.shared.command.result.Calendar;
import org.sigmah.shared.dto.calendar.ActivityCalendarIdentifier;
import org.sigmah.shared.dto.calendar.Event;
import org.sigmah.shared.dto.referential.GlobalPermissionEnum;
import org.sigmah.shared.util.ProfileUtils;
import com.google.gwt.dom.client.Element;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.DecoratedPopupPanel;
import com.google.gwt.user.client.ui.FlexTable;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.InlineLabel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;
/**
* This widget displays a calendar.
*
* @author Raphaƫl Calabro (rcalabro@ideia.fr)
*/
@SuppressWarnings("deprecation")
public class CalendarWidget extends Composite {
public static final int CELL_DEFAULT_WIDTH = 150;
public static final int CELL_DEFAULT_HEIGHT = 80;
public interface CalendarListener {
void afterRefresh();
}
public interface Delegate {
void edit(Event event, CalendarWidget calendarWidget);
void delete(Event event, CalendarWidget calendarWidget);
}
/**
* Types of displays availables for a calendar.
*
* @author rca
*/
public static enum DisplayMode {
DAY(1, 1) {
@Override
public Date getStartDate(Date date, int firstDay) {
return new Date(date.getYear(), date.getMonth(), date.getDate());
}
@Override
public void nextDate(Date currentDate) {
currentDate.setDate(currentDate.getDate() + 1);
}
@Override
public void previousDate(Date currentDate) {
currentDate.setDate(currentDate.getDate() - 1);
}
@Override
public void firstDay(Date currentDate, Date today, int firstDay) {
currentDate.setYear(today.getYear());
currentDate.setMonth(today.getMonth());
currentDate.setDate(today.getDate());
}
@Override
public String getStyleName() {
return "calendar-day";
}
},
WEEK(7, 1) {
@Override
public Date getStartDate(Date date, int firstDay) {
return getFirstDateOfWeek(date, firstDay);
}
@Override
public void nextDate(Date currentDate) {
currentDate.setDate(currentDate.getDate() + 7);
}
@Override
public void previousDate(Date currentDate) {
currentDate.setDate(currentDate.getDate() - 7);
}
@Override
public void firstDay(Date currentDate, Date today, int firstDay) {
int decal = (today.getDay() + 7 - firstDay) % 7;
currentDate.setYear(today.getYear());
currentDate.setMonth(today.getMonth());
currentDate.setDate(today.getDate() - decal);
final Date date = getFirstDateOfWeek(today, firstDay);
currentDate.setTime(date.getTime());
}
@Override
public String getStyleName() {
return "calendar-week";
}
},
MONTH(7, 6) {
@Override
public Date getStartDate(Date date, int firstDay) {
Date firstDayOfMonth = new Date(date.getYear(), date.getMonth(), 1);
return getFirstDateOfWeek(firstDayOfMonth, firstDay);
}
@Override
public void nextDate(Date currentDate) {
currentDate.setMonth(currentDate.getMonth() + 1);
}
@Override
public void previousDate(Date currentDate) {
currentDate.setMonth(currentDate.getMonth() - 1);
}
@Override
public void firstDay(Date currentDate, Date today, int firstDay) {
currentDate.setYear(today.getYear());
currentDate.setMonth(today.getMonth());
currentDate.setDate(1);
}
@Override
public String getStyleName() {
return "calendar-month";
}
};
private int columns;
private int rows;
private DisplayMode(int columns, int rows) {
this.columns = columns;
this.rows = rows;
}
public int getRows() {
return rows;
}
public int getColumns() {
return columns;
}
public abstract Date getStartDate(Date date, int firstDay);
public abstract void nextDate(Date currentDate);
public abstract void previousDate(Date currentDate);
public abstract void firstDay(Date currentDate, Date today, int firstDay);
public abstract String getStyleName();
}
public final static int NO_HEADERS = 0;
public final static int COLUMN_HEADERS = 1;
public final static int ALL_HEADERS = 2;
private final static int UNDEFINED = -1;
private final static int EVENT_HEIGHT = 16;
private int eventLimit = UNDEFINED;
private int firstDayOfWeek;
private DisplayMode displayMode = DisplayMode.MONTH;
private int displayHeaders = ALL_HEADERS;
private boolean displayWeekNumber = true;
private List<Calendar> calendars;
private Date today;
private Date startDate;
private DateTimeFormat titleFormatter = DateTimeFormat.getFormat("MMMM y");
private DateTimeFormat headerFormatter = DateTimeFormat.getFormat("EEEE");
private DateTimeFormat dayFormatter = DateTimeFormat.getFormat("d");
private DateTimeFormat hourFormatter = DateTimeFormat.getFormat("HH:mm");
private CalendarListener listener;
private Delegate delegate;
private final Authentication authentication;
public CalendarWidget(int displayHeaders, boolean displayWeekNumber, Authentication authentication) {
this.calendars = new ArrayList<Calendar>();
this.displayHeaders = displayHeaders;
this.displayWeekNumber = displayWeekNumber;
this.authentication = authentication;
// final SimplePanel container;
final FlexTable grid = new FlexTable();
grid.addStyleName("calendar");
grid.addStyleName(displayMode.getStyleName());
initWidget(grid);
final Date now = new Date();
today = new Date(now.getYear(), now.getMonth(), now.getDate());
startDate = new Date(0, 0, 0);
today();
}
public void setDelegate(Delegate delegate) {
this.delegate = delegate;
}
public void setListener(CalendarListener listener) {
this.listener = listener;
}
public void next() {
displayMode.nextDate(startDate);
refresh();
}
public void previous() {
displayMode.previousDate(startDate);
refresh();
}
public final void today() {
displayMode.firstDay(startDate, today, firstDayOfWeek);
refresh();
}
/**
* Retrieves the current start date of the calendar.
*
* @return the current start date of the calendar.
*/
public Date getStartDate() {
return startDate;
}
public void addCalendar(Calendar calendar) {
calendars.add(calendar);
refresh();
}
public List<Calendar> getCalendars() {
return calendars;
}
public void setCalendars(List<Calendar> calendars) {
this.calendars = calendars;
refresh();
}
/**
* Defines the formatter used to display the title of the calendar.<br>
* <br>
* The default format is "<code>MonthName</code> <code>FullYear</code> " (pattern : "N y").
*
* @param titleFormatter
* The formatter to use to display the title of the calendar.
*/
public void setTitleFormatter(DateTimeFormat titleFormatter) {
this.titleFormatter = titleFormatter;
refresh();
}
/**
* Defines the formatter used to display the title of each column.<br>
* <br>
* The default format is "<code>WeekName</code>" (pattern : "E").
*
* @param headerFormatter
* The formatter to use to display the title of each column.
*/
public void setHeaderFormatter(DateTimeFormat headerFormatter) {
this.headerFormatter = headerFormatter;
refresh();
}
/**
* Defines the formatter used to display the title of each cell.<br>
* <br>
* The default format is "<code>DayNumber</code>" (pattern : "d").
*
* @param dayFormatter
* The formatter to use to display the title of each cell.
*/
public void setDayFormatter(DateTimeFormat dayFormatter) {
this.dayFormatter = dayFormatter;
refresh();
}
/**
* Defines the display mode of the calendar and perform a redraw.
*
* @param displayMode
* Style of the calendar (day, week or month).
* @see CalendarWidget.DisplayMode
*/
public void setDisplayMode(DisplayMode displayMode) {
final FlexTable grid = (FlexTable) getWidget();
clear();
// Resetting the CSS style
grid.removeStyleName(this.displayMode.getStyleName());
this.displayMode = displayMode;
// Applying the CSS style associated with the new display mode
grid.addStyleName(displayMode.getStyleName());
refresh();
}
/**
* Defines the first day of the week and refresh the calendar.
*
* @param firstDayOfWeek
* The first day of the week as an int (Sunday = 0, Saturday = 6)
*/
public void setFirstDayOfWeek(int firstDayOfWeek) {
this.firstDayOfWeek = firstDayOfWeek;
refresh();
}
public int getDisplayHeaders() {
return displayHeaders;
}
public void setDisplayHeaders(int displayHeaders) {
clear();
this.displayHeaders = displayHeaders;
refresh();
}
public boolean isDisplayWeekNumber() {
return displayWeekNumber;
}
public void setDisplayWeekNumber(boolean displayWeekNumber) {
clear();
this.displayWeekNumber = displayWeekNumber;
refresh();
}
/**
* Removes all rows. Must be when the structure of the calendar has been changed (display mode)
*/
private void clear() {
final FlexTable grid = (FlexTable) getWidget();
grid.clear();
grid.removeAllRows();
}
/**
* @param date1
* @param date2
* @return boolean indicating if date1 and date2 are on the same day
*/
public static boolean isSameDay(Date date1, Date date2) {
DateTimeFormat fmt = DateTimeFormat.getFormat("yyyyMMdd");
return fmt.format(date1).equals(fmt.format(date2));
}
/**
* Normalizes the given {@code calendar}'s events map (needed particularly when there is a timezone difference between
* the client and the server).
*
* @param calendar
* The calendar instance.
* @return The map with each event with the right key.
*/
public static Map<Date, List<Event>> normalize(final Calendar calendar) {
final Map<Date, List<Event>> eventMap = calendar.getEvents();
final Map<Date, List<Event>> eventMapNormalized = new HashMap<Date, List<Event>>();
boolean isActivityCalendar = false;
if (calendar.getIdentifier() instanceof ActivityCalendarIdentifier) {
isActivityCalendar = true;
}
for (final Date key : eventMap.keySet()) {
for (final Event event : eventMap.get(key)) {
Date normalizedKeyDate = new Date(key.getYear(), key.getMonth(), key.getDate());
// Activities events have different startDate from the key date
// They shouldn't be placed in their startDate list
if (!isSameDay(normalizedKeyDate, event.getDtstart()) && !isActivityCalendar) {
normalizedKeyDate = new Date(event.getDtstart().getYear(), event.getDtstart().getMonth(), event.getDtstart().getDate());
}
if (eventMapNormalized.get(normalizedKeyDate) == null) {
eventMapNormalized.put(normalizedKeyDate, new ArrayList<Event>());
}
eventMapNormalized.get(normalizedKeyDate).add(event);
}
}
return eventMapNormalized;
}
/**
* Calculates the number of events that can be displayed in a cell.
*/
public void calibrateCalendar() {
final FlexTable grid = (FlexTable) getWidget();
final Element row = grid.getRowFormatter().getElement(displayHeaders);
row.setId("calendar-row-calibration");
final Element cell = grid.getCellFormatter().getElement(displayHeaders, displayWeekNumber ? 1 : 0);
cell.setId("calendar-cell-calibration");
eventLimit = (getCellHeight(CELL_DEFAULT_HEIGHT) / EVENT_HEIGHT) - 2;
if (eventLimit < 0)
eventLimit = 0;
}
/**
* Calculates the height of the cell identified by "calendar-cell-calibration".
*
* @return height of a cell.
*/
private native int getCellHeight(int defaultHeight) /*-{
var height = 0;
if (!$wnd.getComputedStyle)
return defaultHeight;
var row = $wnd.document.getElementById('calendar-row-calibration');
var style = $wnd.getComputedStyle(row, null);
height += parseInt(style.height);
return height;
}-*/;
/**
* Calculates the width of the cell identified by "calendar-cell-calibration".
*
* @return width of a cell.
*/
private native int getCellWidth(int defaultWidth) /*-{
var width = 0;
if (!$wnd.getComputedStyle)
return defaultWidth;
var cell = $wnd.document.getElementById('calendar-cell-calibration');
var style = $wnd.getComputedStyle(cell, null);
width += parseInt(style.width);
return width;
}-*/;
/**
* Retrieves the current heading of the calendar.
*
* @return The heading value.
*/
public String getHeading() {
final String title = titleFormatter.format(startDate);
return Character.toUpperCase(title.charAt(0)) + title.substring(1);
}
/**
* Render the calendar.
*/
public void refresh() {
drawEmptyCells();
if (isAttached()) {
calibrateCalendar();
drawEvents();
}
if (listener != null)
listener.afterRefresh();
}
/**
* Render the whole calendar but do not render the events.
*/
public void drawEmptyCells() {
final FlexTable grid = (FlexTable) getWidget();
final int rows = displayMode.getRows() + displayHeaders;
final int columns = displayMode.getColumns() + (displayWeekNumber ? 1 : 0);
Date date = displayMode.getStartDate(startDate, firstDayOfWeek);
// Column headers
if (displayHeaders != NO_HEADERS) {
if (displayHeaders == ALL_HEADERS) {
// Header of the calendar
final Label calendarHeader = new Label(getHeading());
calendarHeader.addStyleName("calendar-header");
grid.setWidget(0, 0, calendarHeader);
grid.getFlexCellFormatter().setColSpan(0, 0, columns + (displayWeekNumber ? 1 : 0));
}
final Date currentHeader = new Date(date.getTime());
for (int x = displayWeekNumber ? 1 : 0; x < columns; x++) {
final Label columnHeader = new Label(headerFormatter.format(currentHeader));
columnHeader.addStyleName("calendar-column-header");
grid.setWidget(displayHeaders == ALL_HEADERS ? 1 : 0, x, columnHeader);
currentHeader.setDate(currentHeader.getDate() + 1);
}
}
int currentMonth = startDate.getMonth();
for (int y = displayHeaders; y < rows; y++) {
if (displayWeekNumber) {
grid.getCellFormatter().addStyleName(y, 0, "calendar-row-header");
grid.setText(y, 0, Integer.toString(getWeekNumber(date, firstDayOfWeek)));
}
for (int x = displayWeekNumber ? 1 : 0; x < columns; x++) {
drawCell(y, x, date, currentMonth);
date.setDate(date.getDate() + 1);
}
}
}
/**
* Render the events for every cells.
*/
public void drawEvents() {
final int rows = displayMode.getRows() + displayHeaders;
final int columns = displayMode.getColumns() + (displayWeekNumber ? 1 : 0);
Date date = displayMode.getStartDate(startDate, firstDayOfWeek);
for (int y = displayHeaders; y < rows; y++) {
for (int x = displayWeekNumber ? 1 : 0; x < columns; x++) {
drawEvents(y, x, date);
date.setDate(date.getDate() + 1);
}
}
}
/**
* Render the cell located at <code>column</code>, <code>row</code>
*
* @param row
* @param column
* @param date
* @param currentMonth
*/
private void drawCell(int row, int column, Date date, int currentMonth) {
final Label header = new Label(dayFormatter.format(date));
header.addStyleName("calendar-cell-header");
final FlexTable grid = (FlexTable) getWidget();
grid.getCellFormatter().setStyleName(row, column, "calendar-cell");
FlowPanel cell = (FlowPanel) grid.getWidget(row, column);
if (cell == null) {
// New cell
cell = new FlowPanel();
cell.setWidth("100%");
grid.setWidget(row, column, cell);
} else {
// Reusing an existing cell
cell.clear();
}
if (currentMonth != date.getMonth())
grid.getCellFormatter().addStyleName(row, column, "calendar-cell-other-month");
if (date.equals(today))
grid.getCellFormatter().addStyleName(row, column, "calendar-cell-today");
cell.add(header);
}
/**
* Display the events for the cell located at <code>column</code>, <code>row</code>
*
* @param row
* @param column
* @param date
* @param currentMonth
*/
private void drawEvents(int row, int column, final Date date) {
final FlexTable grid = (FlexTable) getWidget();
// final VerticalPanel cell = (VerticalPanel) grid.getWidget(row,
// column);
final FlowPanel cell = (FlowPanel) grid.getWidget(row, column);
if (cell == null)
throw new NullPointerException("The specified cell (" + row + ',' + column + ") doesn't exist.");
// Displaying events
final TreeSet<Event> sortedEvents = new TreeSet<Event>(new Comparator<Event>() {
@Override
public int compare(Event o1, Event o2) {
int compare = 0;
if (o1 == null && o2 == null)
return 0;
else if (o2 == null)
return 1;
else if (o1 == null)
return -1;
if (compare == 0 && o1.getDtstart() != null && o2.getDtstart() != null) {
long o1Start = o1.getDtstart().getTime();
long o2Start = o2.getDtstart().getTime();
if (o1Start < o2Start)
compare = -1;
else if (o1Start > o2Start)
compare = 1;
}
if (compare == 0 && o1.getSummary() != null && o2.getSummary() != null)
compare = o1.getSummary().compareTo(o2.getSummary());
return compare;
}
});
for (final Calendar calendar : calendars) {
final Map<Date, List<Event>> eventMap = normalize(calendar);
final List<Event> events = eventMap.get(date);
if (events != null) {
sortedEvents.addAll(events);
}
}
final Iterator<Event> iterator = sortedEvents.iterator();
for (int i = 0; iterator.hasNext() && i < eventLimit; i++) {
final Event event = iterator.next();
final ClickableFlowPanel flowPanel = new ClickableFlowPanel();
flowPanel.addStyleName("calendar-event");
boolean fullDayEvent = false;
final StringBuilder eventDate = new StringBuilder();
eventDate.append(hourFormatter.format(event.getDtstart()));
if (event.getDtend() != null) {
eventDate.append(" ");
eventDate.append(hourFormatter.format(event.getDtend()));
if (event.getDtstart().getDate() != event.getDtend().getDate()
|| event.getDtstart().getMonth() != event.getDtend().getMonth()
|| event.getDtstart().getYear() != event.getDtend().getYear()) {
fullDayEvent = true;
flowPanel.addStyleName("calendar-fullday-event");
}
}
final InlineLabel dateLabel = new InlineLabel(eventDate.toString());
dateLabel.addStyleName("calendar-event-date");
final InlineLabel eventLabel = new InlineLabel(event.getSummary());
eventLabel.addStyleName("calendar-event-label");
if (fullDayEvent)
flowPanel.addStyleName("calendar-fullday-event-" + event.getParent().getStyle());
else
eventLabel.addStyleName("calendar-event-" + event.getParent().getStyle());
if (!fullDayEvent)
flowPanel.add(dateLabel);
flowPanel.add(eventLabel);
final DecoratedPopupPanel detailPopup = new DecoratedPopupPanel(true);
final Grid popupContent = new Grid(event.getParent().isEditable() ? 5 : 3, 1);
popupContent.setText(0, 0, event.getSummary());
popupContent.getCellFormatter().addStyleName(0, 0, "calendar-popup-header");
if (!fullDayEvent) {
popupContent.getCellFormatter().addStyleName(1, 0, "calendar-popup-date");
popupContent.getCellFormatter().addStyleName(1, 0, "calendar-event-" + event.getParent().getStyle());
popupContent.setText(1, 0, eventDate.toString());
} else
popupContent.setText(1, 0, "");
if (event.getDescription() != null && !"".equals(event.getDescription())) {
popupContent.getCellFormatter().addStyleName(2, 0, "calendar-popup-description");
popupContent.setText(2, 0, event.getDescription());
} else
popupContent.setText(2, 0, "");
if (event.getParent().isEditable()
&& ProfileUtils.isGranted(authentication, GlobalPermissionEnum.EDIT_PROJECT_AGENDA)) {
final Anchor editAnchor = new Anchor(I18N.CONSTANTS.calendarEditEvent());
editAnchor.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent clickEvent) {
delegate.edit(event, CalendarWidget.this);
}
});
final Anchor deleteAnchor = new Anchor(I18N.CONSTANTS.calendarDeleteEvent());
deleteAnchor.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent clickEvent) {
delegate.delete(event, CalendarWidget.this);
detailPopup.hide();
}
});
popupContent.setWidget(3, 0, editAnchor);
popupContent.setWidget(4, 0, deleteAnchor);
}
detailPopup.setWidget(popupContent);
flowPanel.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
final int left = flowPanel.getAbsoluteLeft() - 10;
final int bottom = Window.getClientHeight() - flowPanel.getAbsoluteTop();
detailPopup.setWidth((getCellWidth(CELL_DEFAULT_WIDTH) + 20) + "px");
// Show the popup
detailPopup.setPopupPositionAndShow(new PositionCallback() {
@Override
public void setPosition(int offsetWidth, int offsetHeight) {
detailPopup.getElement().getStyle().setPropertyPx("left", left);
detailPopup.getElement().getStyle().setProperty("top", "");
detailPopup.getElement().getStyle().setPropertyPx("bottom", bottom);
}
});
}
});
cell.add(flowPanel);
}
if (eventLimit != UNDEFINED && sortedEvents.size() > eventLimit) {
final Anchor eventLabel = new Anchor("\u25BC");
final Date thisDate = new Date(date.getTime());
eventLabel.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
startDate = thisDate;
setDisplayMode(DisplayMode.WEEK);
}
});
eventLabel.addStyleName("calendar-event-limit");
cell.add(eventLabel);
}
}
/**
* Returns the first date of the week that includes the given date.
*
* @param day
* A date
* @param firstDay
* The first day of the week (such as {@link #SUNDAY}, {@link #MONDAY} or anything else).
* @return The first date of the week that includes <code>day</day>, as a {@link Date}.
*/
private static Date getFirstDateOfWeek(Date day, int firstDay) {
final int decal = (day.getDay() + 7 - firstDay) % 7;
return new Date(day.getYear(), day.getMonth(), day.getDate() - decal);
}
/**
* Calculates the number of the week that includes the given date.
*
* @param date
* A date
* @param firstDay
* The first day of the week (such as {@link #SUNDAY}, {@link #MONDAY} or anything else).
* @return The number of the week that includes <code>date</code>.
*/
private static int getWeekNumber(Date date, int firstDay) {
int daysToThursday = 4 - date.getDay();
if (date.getDay() < firstDay)
daysToThursday -= 7;
final Date thursday = new Date(date.getYear(), date.getMonth(), date.getDate() + daysToThursday);
final Date januaryFourth = new Date(thursday.getYear(), 0, 4);
final int daysToMonday = 1 - januaryFourth.getDay(); // Essayer avec le
// 1er jour de
// la
// semaine
final Date monday = new Date(thursday.getYear(), 0, 4 + daysToMonday);
final double diff = Math.floor((thursday.getTime() - monday.getTime()) / (1000 * 60 * 60 * 24));
return (int) Math.ceil(diff / 7.0);
}
}